package com.googlecode.lightest.core

import org.codehaus.groovy.control.ConfigurationException 
import org.codehaus.groovy.runtime.StackTraceUtils

import org.testng.ITestNGListener

/**
 * Represents a configuration of the test run
 */
// TODO - separate Configuration value object from a ConfigurationFactory
class Configuration implements IConfiguration {
    public static final IPreferences UNSPECIFIED_PREFS = [:] as IPreferences
    public static final List<ITestEnvironment> UNSPECIFIED_ENVS = [
        new TestEnvironment('unspecified1'),
        new TestEnvironment('unspecified2'),
        new TestEnvironment('unspecified3')
    ]
    
    public static final String DEFAULT_REPORTER =
        'com.googlecode.lightest.core.DefaultReporter'
    public static final String DEFAULT_ASSIGNMENT_STRATEGY =
        'com.googlecode.lightest.core.SimpleDispatcherAssignmentStrategy'
    
    private classLoader
    private classPaths
    private outputDir
    private prefs
    private envs
    private List reporters
    private List<ITestNGListener> testNGListeners
    private String contextClass
    private IDispatcherAssignmentStrategy assignmentStrategy
    private ITaskDispatchStrategy dispatchStrategy

    Configuration() {
        classLoader = new GroovyClassLoader(this.class.classLoader)
    }

    /**
     * Initializes the configuration.
     *
     * @param configText  a text representation of the configuration to
     *                    initialize. See the Lightest documentation for the
     *                    format of this text.
     */
    void init(String configText) {
        Node config

        if (configText =~ /\S/) {
            def header = 'def config = new NodeBuilder()\n'

            try {
                config = Eval.me(header + configText)
            }
            catch (e) {
                println 'Caught exception parsing the following configuration:'
                println configText
                StackTraceUtils.sanitize(e)
                throw e
            }
        }
        else {
            config = new Node(null, 'config')
        }

        setClassPaths(config)
        setOutputDir(config)
        setPreferences(config)
        setEnvironments(config)
        setReporters(config)
        setListeners(config)
        setContextClass(config)
        setDispatcherAssignmentStrategy(config)
        setTaskDispatchStrategy(config)
    }

    /**
     * Determines if the specified configuration is a valid one. If not, throws
     * an exception. Not implemented.
     */
    void validate() {
        // TODO - implement
    }

    private void setClassPaths(Node config) {
        classPaths = [ '.' ]

        if (config.classPaths.size() > 0) {
            config.classPaths[0].path.each {
                classPaths << it.value()
            }
        }
    }

    private void setOutputDir(Node config) {
        outputDir = config.outputDir[0]?.value() ?: 'lightest-report'
        outputDir = new File(outputDir).getCanonicalPath()
    }

    /**
     * Initializes the preferences instance according to the configuration. If
     * the configuration references a property that is not a property of the
     * preference, an exception is thrown.
     *
     * @param config  the configuration containing the preference information.
     *                It must have a "prefs" child with a "class" attribute.
     */
    private void setPreferences(Node config) {
        if (config.prefs.size() == 0) {
            prefs = UNSPECIFIED_PREFS
            return
        }

        prefs = configureInstance(config.prefs[0])
    }

    private void setEnvironments(Node config) {
        if (config.envs.size() == 0 || config.envs.env.size() == 0) {
            envs = UNSPECIFIED_ENVS
            return
        }

        assert config.envs[0].'@class' != null

        envs = []

        // TODO - is there any reason why all environments have to be of the
        //        same class?
        def envClass = loadClass(config.envs.'@class')

        for (envConfig in config.envs[0].env) {
            validateEnvironment(envConfig, envs)
            
            def env = envClass.newInstance()
            
            env.setId(envConfig.'@id')
            env.initializeProperties(envConfig)

            envs << env
        }
    }
    
    // TODO - replace validation with groovytools-builder metabuilder schema
    private void validateEnvironment(envConfig, envs) {
        def id = envConfig.'@id'
        
        if (! id) {
            def msg = ("The 'id' attribute must be specified for the declared "
                + "environment ${envConfig}")
            throw new ConfigurationException(msg)
        }
        else if (envs.find { it.getId() == id }) {
            def msg = "The id '${id}' is not unique among declared environments"
            throw new ConfigurationException(msg)
        }
    }
    
    private void setContextClass(Node config) {
        contextClass = (config.context[0]?.'@class' ?:
            LightestContextFactory.DEFAULT_CONTEXT_CLASS)
    }

    private void setReporters(Node config) {
        if (config.reporters.size() == 0) {
            reporters = [ loadClass(DEFAULT_REPORTER).newInstance() ]
        }
        else {
            reporters = []
            
            config.reporters[0].reporter.each {
                reporters << configureInstance(it)
            }
        }
    }
    
    private void setListeners(Node config) {
        if (config.listeners.size() > 0) {
            testNGListeners = []
            
            config.listeners[0].listener.each {
                testNGListeners << configureInstance(it)
                
            }
        }
    }

    private void setDispatcherAssignmentStrategy(Node config) {
        def strategyClass = config.dispatcherAssignmentStrategy.'@class'
        assignmentStrategy = loadClass(strategyClass ?:
            DEFAULT_ASSIGNMENT_STRATEGY).newInstance()
    }
    
    private void setTaskDispatchStrategy(Node config) {
        if (config.taskDispatchStrategy.size() == 0) {
            return
        }
        
        dispatchStrategy = configureInstance(config.taskDispatchStrategy[0])
    }
    
    /**
     * Loads the class using a GroovyClassLoader whose classpath is set to
     * include all paths that have been parsed from the configuration text.
     *
     * @param className
     */
    private Class loadClass(String className) {
        LightestUtils.addClassPaths(classPaths, classLoader)
        return classLoader.loadClass(className, true)
    }
    
    /**
     * Returns a new instance of a class, as defined in a configuration node.
     * The class' binary name must be specified in the "class" attribute, and
     * the node's children are considered name-value pairs that can be used
     * to set properties on the new instance. The class loader used to load
     * the class will include at least all paths returned by getClassPaths().
     * 
     * @param instanceConfig
     */
    protected Object configureInstance(Node instanceConfig) {
        assert instanceConfig.'@class' != null
        
        def instance = loadClass(instanceConfig.'@class').newInstance()
        
        instanceConfig.children().each {
            def propName = it.name()
            def metaClass = instance.class.metaClass
            
            if (metaClass.getMetaProperty(propName)) {
                instance."${propName}" = it.value()
                return
            }
            
            def pluralPropName = "${propName}s"
            
            if (metaClass.getMetaProperty(pluralPropName)) {
                def prop = instance."${pluralPropName}"
                if (prop != null && prop instanceof Collection) {
                    instance."${pluralPropName}" << it.value()
                    return
                }
            }
        }
        
        return instance
    }

    /**
     * Returns the class loader used to load the configuration-related classes.
     */
    GroovyClassLoader getClassLoader() {
        return classLoader
    }

    /**
     * Returns a List of Strings representing class paths under which tasks
     * and potentially environment and preference classes are defined. The
     * current directory is added by default.
     */
    List<String> getClassPaths() {
        return classPaths
    }

    /**
     * Returns the output directory for the test run report. The default output
     * directory is "lightest-report".
     */
    String getOutputDir() {
         return outputDir
     }

    /**
     * Returns an instance of the concrete implementation of IPreferences.
     */
    IPreferences getPreferences() {
        return prefs
    }

    /**
     * Returns a List of ITestEnvironment instances. The UNSPECIFIED_ENVS list
     * containing three default environment entries will be returned if
     * unspecified.
     */
    List<ITestEnvironment> getEnvironments() {
        return envs
    }
    
    String getContextClass() {
        return contextClass
    }

    List getReporters() {
        return reporters
    }
    
    List<ITestNGListener> getListeners() {
        return testNGListeners ?: []
    }

    /**
     * Returns the specified dispatcher assignment strategy. A DEFAULT_STRATEGY
     * will be returned if unspecified.
     */
    IDispatcherAssignmentStrategy getDispatcherAssignmentStrategy() {
        return assignmentStrategy
    }
    
    ITaskDispatchStrategy getTaskDispatchStrategy() {
        return dispatchStrategy
    }
}
